// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;

using Tierless.Framework.Data;
using Tierless.Framework.EntityTypes;

namespace Tierless.Framework.Entities
{
	/// <summary>
	/// Represents the ID of an entity.
	/// </summary>
	[DataContract]
	public class EntityID : IComparable, IComparable<EntityID>, ICloneable
	{
		/// <summary>
		/// Initializes a new instance of the EntityID class.
		/// </summary>
		/// <param name="entity">The entity of the ID.</param>
		/// <param name="key">The key of the ID.</param>
		public EntityID(Entity entity, EntityKey key)
		{
			if (entity == null) throw new ArgumentNullException("AEntity");
			if (key == null) throw new ArgumentNullException("AKey");

			_columns = new string[key.Columns.Count];
			_data = new object[_columns.Length];
			for (var i = 0; i < _columns.Length; i++)
			{
				_columns[i] = key.Columns[i].Name;
				_data[i] = entity[_columns[i]].Value;
			}
		}

		/// <summary>
		/// Initializes a new instance of the EntityID class.
		/// </summary>
		/// <param name="entity">The entity of the ID.</param>
		/// <param name="columnNames">The names of the columns of the key of the ID.</param>
		public EntityID(Entity entity, string[] columnNames)
		{
			if (entity == null) throw new ArgumentNullException("AEntity");
			if (columnNames == null) throw new ArgumentNullException("AColumnNames");

			_columns = columnNames;
			_data = new object[_columns.Length];
			for (var i = 0; i < _columns.Length; i++)
				_data[i] = entity[_columns[i]].Value;
		}

		/// <summary>
		/// Initializes a new instance of the EntityID class.
		/// </summary>
		/// <param name="key">The key of the ID.</param>
		/// <param name="data">The values of the columns of the ID.</param>
		public EntityID(EntityKey key, params object[] data)
		{
			if (key == null) throw new ArgumentNullException("AKey");
			if (data == null) throw new ArgumentNullException("AData");
			if (key.Columns.Count != data.Length) throw new ArgumentException("Key must have the same number of columns as the number of data values provided.");

			_columns = new string[key.Columns.Count];
			for (var i = 0; i < _columns.Length; i++)
				_columns[i] = key.Columns[i].Name;
			_data = data;
		}

		/// <summary>
		/// Initializes a new instance of the EntityID class.
		/// </summary>
		/// <param name="key">The key of the ID.</param>
		/// <remarks>
		/// After initialization, the columns of the key of the ID will be null.
		/// </remarks>
		public EntityID(EntityKey key)
		{
			if (key == null) throw new ArgumentNullException("AKey");
			_columns = new string[key.Columns.Count];
			for (var i = 0; i < _columns.Length; i++)
				_columns[i] = key.Columns[i].Name;
			_data = new object[_columns.Length];
		}

		/// <summary>
		/// Initializes a new instance of the EntityID class.
		/// </summary>
		/// <param name="columnNames">The column names of the key of the ID.</param>
		/// <param name="data">The values of the columns of the key of the ID.</param>
		public EntityID(string[] columnNames, params object[] data)
		{
			if (columnNames == null) throw new ArgumentNullException("AColumnNames");
			if (data == null) throw new ArgumentNullException("AData");
			if (columnNames.Length != data.Length) throw new ArgumentException("Number of columns must be the same as the number of data values provided.");

			_columns = columnNames;
			_data = data;
		}
		
		/// <summary>
		/// Initializes a new instance of the EntityID class, based on another EntityID of the same type.
		/// </summary>
		/// <param name="columnNames">The column names of the key of the ID.</param>
		/// <param name="entityID">The ID to use to initialize the values of the new key.</param>
		public EntityID(string[] columnNames, EntityID entityID) : this(columnNames, entityID._data) { }

		/// <summary>
		/// Initializes a new instance of the EntityID class.
		/// </summary>
		/// <param name="columnNames">The column names of the key of the ID.</param>
		public EntityID(string[] columnNames)
		{
			if (columnNames == null) throw new ArgumentNullException("AColumnNames");

			_columns = columnNames;
			_data = new object[_columns.Length];
		}

		[DataMember]
		private string[] _columns;

		[DataMember]
		private object[] _data;

		/// <summary>
		/// Returns the index of the key column with the given name if it exists, -1 otherwise.
		/// </summary>
		/// <param name="columnName">The name of the key column.</param>
		/// <returns>The index of the key column with the given name if it exists, -1 otherwise.</returns>
		public int IndexOf(string columnName)
		{
			for (var i = 0; i < _columns.Length; i++)
				if (_columns[i] == columnName)
					return i;
			return -1;
		}

		/// <summary>
		/// Returns true if the key contains a column with the given name, false otherwise.
		/// </summary>
		/// <param name="columnName">The name of the key column.</param>
		/// <returns>True if the key contains a column with the given name, false otherwise.</returns>
		public bool Contains(string columnName)
		{
			return IndexOf(columnName) >= 0;
		}

		/// <summary>
		/// Gets the number of columns in the key.
		/// </summary>
		public int ColumnCount
		{
			get { return _columns.Length; }
		}

		/// <summary>
		/// Gets the name of the column at the given index.
		/// </summary>
		/// <param name="index">The index of the column.</param>
		/// <returns>The name of the column at the given index.</returns>
		public string ColumnName(int index)
		{
			return _columns[index];
		}

		/// <summary>
		/// Gets the value of the column at the given index.
		/// </summary>
		/// <param name="index">The index of the column.</param>
		/// <returns>The value of the column at the given index.</returns>
		public object this[int index]
		{
			get { return _data[index]; }
		}

		/// <summary>
		/// Gets the value of the column with the given name.
		/// </summary>
		/// <param name="columnName">The name of the column.</param>
		/// <returns>The value of the column with the given name.</returns>
		public object this[string columnName]
		{
			get { return this[IndexOf(columnName)]; }
		}

		/// <summary>
		/// Provides value-based equality semantics for the EntityID.
		/// </summary>
		/// <param name="obj">The EntityID to compare.</param>
		/// <returns>True if the given EntityID is of the same type, and has the same values for all columns.</returns>
		public override bool Equals(object obj)
		{
			var other = obj as EntityID;
			if ((!ReferenceEquals(other, null)) && (ColumnCount == other.ColumnCount))
			{
				return !_columns.Where((AColumn, i) => !Equals(this[i], other[i])).Any();
			}
			return false;
		}

		public override int GetHashCode()
		{
			var code = 0;

			for (var i = 0; i < ColumnCount; i++)
				code = (code * 83) + (this[i] == null ? 0 : this[i].GetHashCode());

			return code;
		}

		public override string ToString()
		{
			var result = new StringBuilder();

			for (var i = 0; i < ColumnCount; i++)
			{
				if (result.Length > 0)
					result.Append(", ");
				result.AppendFormat("{0}: {1}", _columns[i], this[i] == null ? "<no value>" : this[i].ToString());
			}

			return result.ToString();
		}

		#region IComparable Members

		int IComparable.CompareTo(object obj)
		{
			return CompareTo(obj as EntityID);
		}

		#endregion

		#region IComparable<EntityID> Members

		/// <summary>
		/// Provides value-based comparison semantics for the EntityID class.
		/// </summary>
		/// <param name="other"></param>
		/// <returns>-1 if this EntityID is less than the other, 0 if this EntityID is equal to the other, and 1 if this EntityID is greater than the other.</returns>
		public int CompareTo(EntityID other)
		{
			if (other == null) throw new ArgumentNullException("AOther");
			if (ColumnCount != other.ColumnCount)
				throw new ArgumentException("Cannot compare keys with different numbers of columns.");

			var r = 0;
			for (var i = 0; i < ColumnCount; i++)
			{
				if (this[i] != null)
				{
					if (other[i] != null)
					{
						var left = this[i] as IComparable;
						var right = other[i] as IComparable;
						if ((left == null) || (right == null))
							throw new InvalidOperationException("IDs cannot be compared because they contain values which do not support IComparable.");

						r = left.CompareTo(right);
					}
					else
						r = 1;
				}
				else
				{
					if (other[i] != null)
						r = -1;
					else
						r = 0;
				}

				if (r != 0)
					break;
			}
			return r;
		}

		#endregion

		/// <summary>
		/// Provides a deep clone of this EntityID.
		/// </summary>
		/// <returns>A deep clone of this EntityID.</returns>
		public EntityID Clone()
		{
			return new EntityID((string[]) _columns.Clone(), (object[]) _data.Clone());
		}

		#region ICloneable Members

		object ICloneable.Clone()
		{
			return Clone();
		}

		#endregion

		public static bool operator ==(EntityID left, EntityID right)
		{
			return EntityIDEqualityComparer.Default.Equals(left, right);
		}

		public static bool operator !=(EntityID left, EntityID right)
		{
			return !EntityIDEqualityComparer.Default.Equals(left, right);
		}

		public static bool operator >(EntityID left, EntityID right)
		{
			return EntityIDComparer.Default.Compare(left, right) > 0;
		}

		public static bool operator >=(EntityID left, EntityID right)
		{
			return EntityIDComparer.Default.Compare(left, right) >= 0;
		}

		public static bool operator <(EntityID left, EntityID right)
		{
			return EntityIDComparer.Default.Compare(left, right) < 0;
		}

		public static bool operator <=(EntityID left, EntityID right)
		{
			return EntityIDComparer.Default.Compare(left, right) <= 0;
		}
		
		public EntityParameter[] ToEntityParameterArray(string entityTypeName)
		{
			var array = new EntityParameter[_columns.Length];
			for (var i = 0; i < _columns.Length; i++)
				array[i] = new EntityParameter(entityTypeName, _columns[i], this[i]);
			return array;
		}

		public bool IsNull
		{
			get { return _data.Any(v => v == null); }
		}
	}

	/// <summary>
	/// Provides an implementation of IComparer[T] for the <see cref="EntityID"/> class.
	/// </summary>
	public class EntityIDComparer : IComparer<EntityID>
	{
		private static EntityIDComparer _comparer;

		/// <summary>
		/// Gets a default static instance of the comparer.
		/// </summary>
		public static EntityIDComparer Default
		{
			get { return _comparer ?? (_comparer = new EntityIDComparer()); }
		}

		#region IComparer<EntityID> Members

		/// <summary>
		/// Compares two EntityID instances.
		/// </summary>
		/// <param name="x">The left EntityID.</param>
		/// <param name="y">The right EntityID.</param>
		/// <returns>-1 if left is less than right, 0 if they are equal, and 1 if left is greater than right.</returns>
		public int Compare(EntityID x, EntityID y)
		{
			if (x == null) throw new ArgumentNullException("x");

			return x.CompareTo(y);
		}

		#endregion
	}

	/// <summary>
	/// Provides an implementation of IEqualityComparer[T] for the EntityID class.
	/// </summary>
	public class EntityIDEqualityComparer : IEqualityComparer<EntityID>
	{
		private static EntityIDEqualityComparer _comparer;

		/// <summary>
		/// Gets a default static instance of the comparer.
		/// </summary>
		public static EntityIDEqualityComparer Default
		{
			get { return _comparer ?? (_comparer = new EntityIDEqualityComparer()); }
		}

		#region IEqualityComparer<EntityID> Members

		/// <summary>
		/// Returns true if the given instances are equal, false otherwise.
		/// </summary>
		/// <param name="x">The left instance.</param>
		/// <param name="y">The right instance.</param>
		/// <returns>True if the given instances are equal, false otherwise.</returns>
		public bool Equals(EntityID x, EntityID y)
		{
			if (!ReferenceEquals(x, null))
				return x.Equals(y);
			else if (!ReferenceEquals(y, null))
				return y.Equals(x);
			else
				return true;
		}

		public int GetHashCode(EntityID obj)
		{
			return obj.GetHashCode();
		}

		#endregion
	}
}